import datetime
import optparse
import os
import re
from xml.sax.saxutils import escape

import util


RE_BOOK_XML_INCLUDE = re.compile(util.RE_BOOK_XML_CHAPTER_PAT % '([^"]*)')

XML_ATTR_ESCAPES = { '"': '&quot;', "'": '&apos;', '\n': '&#10;' }

COLLAPSE_WHITESPACE_ELEMENTS = (
    'listitem',
    'para',
    )
COLLAPSE_WHITESPACE_ELEMENT_PATTERN = '|'.join(COLLAPSE_WHITESPACE_ELEMENTS)
RE_COLLAPSE_WHITESPACE_ELEM = re.compile(
    '(<(?:%s) ?[^>]*>.*?</(?:%s)>)' %
    (COLLAPSE_WHITESPACE_ELEMENT_PATTERN, COLLAPSE_WHITESPACE_ELEMENT_PATTERN),
    re.DOTALL)

OPML_HEADER_PAT = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<opml version="1.0">
    <head>
        <title></title>
        <dateCreated>%s</dateCreated>
    </head>
    <body>
"""

OPML_FOOTER = """
    </body>
</opml>
"""


def collapse_whitespace(text):
    # TODO: This should probably nest-indent block elements, except
    # for literal elements.
    
    parts = RE_COLLAPSE_WHITESPACE_ELEM.split(text)
    for i in xrange(1, len(parts), 2):
        parts[i] = re.sub(r'\s+', ' ', parts[i])
    return ''.join(parts)


def strip_production_markup(text):
    text = re.sub(r'<indexterm[^>]*>.*?</indexterm>', '', text)
    text = re.sub(r'<indexterm.*?/>', '', text)
    text = re.sub(r'<\?dbfo-.*?\?>', '', text)
    text = re.sub(r'<phrase role="keep-together">(.*?)</phrase>', r'\1', text)
    return text


def unpara(text):
    text = collapse_whitespace(text)
    text = strip_production_markup(text)

    regexp_parts = util.RE_BLOCK_ELEM.split(text)
    parts = []
    for i in xrange(0, len(regexp_parts), 3):
        parts.append(regexp_parts[i])
        if i < len(regexp_parts) - 1:
            parts.append(regexp_parts[i+1])

    for i in xrange(0, len(parts), 2):
        paragraphs = util.RE_PARA_BREAK.split(parts[i])
        for j, p in enumerate(paragraphs):
            p = re.sub(r'<para(\s+[^>]*)?>', '', p)
            p = re.sub(r'</para\s*>', '\n\n', p)
            p = re.sub(r'\n\n\n+', '\n\n', p)
            p = p.strip()
            paragraphs[j] = p
        paragraphs = [p.strip() for p in paragraphs if p.strip()]
        if paragraphs:
            parts[i] = '\n\n'.join(paragraphs)
        else:
            parts[i] = ''

    text = '\n\n'.join(parts).strip()
    return text


def _make_section_re(tag):
    re_top_section = re.compile(
        r'<%s(?: id="([^"]*)"[^>]*)?>\s*<title>(.*?)</title>(.*?)</%s>' %
        (tag, tag), re.DOTALL)
    return re_top_section


def _parse_docbook_section(data, tag, depth, parent=None):
    sections = []

    re_top_section = _make_section_re(tag)
    chunks = re_top_section.split(data)
    chunks.pop()  # discard post-match element

    while len(chunks) >= 4:
        before_unused, id, title, text = chunks[0:4]
        del chunks[0:4]
        inner_section_tag = 'sect' + str(depth + 1)
        inner_section_tag_index = text.find('<' + inner_section_tag)
        outer_text = unpara(text[:inner_section_tag_index])
        section = util.Section(id, title, outer_text, parent=parent)

        if inner_section_tag_index != -1:
            inner_data = text[inner_section_tag_index:]
            section.inner_sections = _parse_docbook_section(
                inner_data, inner_section_tag, depth + 1, parent=section)

        sections.append(section)

    return sections


def parse_docbook(doc, data, _tag=None, _depth=None):
    doc.top_sections = _parse_docbook_section(
        data, '(?:preface|chapter)', 0)


def make_document_from_book_xml_file(book_filename):
    parts = []

    basedir = os.path.dirname(book_filename)
    fh = open(book_filename)
    for line in fh:
        m = RE_BOOK_XML_INCLUDE.match(line)
        if not m:
            parts.append(line)
        else:
            include_filename = m.group(1)
            include_filepath = os.path.join(basedir, include_filename)
            if not os.path.isfile(include_filepath):
                print 'Warning: xi:include of "%s", file not found; ignoring' % include_filename
                continue
            include_fh = open(include_filepath)
            parts.append(include_fh.read())
            include_fh.close()
    fh.close()

    data = '\n'.join(parts)
    doc = util.Document()
    parse_docbook(doc, data)
    return doc


def make_opml_section_text(section):
    title = (' text="%s"' % escape(section.title, XML_ATTR_ESCAPES)
             if section.title else '')
    text = (' _note="%s"' % escape(section.text, XML_ATTR_ESCAPES)
            if section.text else '')
    parts = ['<outline%s%s>' % (title, text)]
    for inner_section in section.inner_sections:
        parts.append(make_opml_section_text(inner_section))
    parts.append('</outline>')
    return '\n'.join(parts)


def make_opml_text(doc):
    parts = [OPML_HEADER_PAT % datetime.datetime.now()]
    parts.extend(make_opml_section_text(section)
                 for section in doc.top_sections)
    parts.append(OPML_FOOTER)
    return ''.join(parts)


def make_opml_file(opml_path, doc):
    fh = open(opml_path, 'w')
    fh.write(make_opml_text(doc))
    fh.close()


def get_option_parser():
    parser = optparse.OptionParser(
        usage='%prog <DocBook-XML-file> <OPML-output-file>')
    return parser


def main(args):
    opt_parser = get_option_parser()
    (options, args) = opt_parser.parse_args(args)

    if len(args) != 2:
        print 'Incorrect number of arguments.'
        opt_parser.print_help()
        return 1

    book_filename = args[0]
    opml_filename = args[1]
    if not opml_filename.endswith('.opml'):
        print ('Warning: Scrivener requires that the OPML file\'s name end\n'
               'with ".opml" in order to recognize it on import. Consider\n'
               'renaming the output file.')

    if not os.path.isfile(book_filename):
        print 'Could not find %s.' % book_filename
        opt_parser.print_help()
        return 1

    doc = make_document_from_book_xml_file(book_filename)
    make_opml_file(opml_filename, doc)

    return 0
